package org.springframework.security.web.authentication.rememberme;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.Assert;

import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;

/**
 * @author Dillon
 * @date 2024/7/11
 * @slogan 致敬大师 致敬未来的你
 * @desc 基于持久化形式的记住我实现类
 */
public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {

	/**
	 * 持久化实现类
	 */
	private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();

	/**
	 * security 随机数类
	 */
	private SecureRandom random;

	/**
	 * 随机数默认长度
	 */
	public static final int DEFAULT_SERIES_LENGTH = 16;

	/**
	 * 默认token长度
	 */
	public static final int DEFAULT_TOKEN_LENGTH = 16;

	/**
	 * 随机数长度
	 */
	private int seriesLength = DEFAULT_SERIES_LENGTH;

	/**
	 * token长度
	 */
	private int tokenLength = DEFAULT_TOKEN_LENGTH;

	/**
	 * 构造函数
	 *
	 * @param key                记住我 tokenKey
	 * @param userDetailsService 数据源实现类
	 * @param tokenRepository    token持久化类
	 */
	public PersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService,
			PersistentTokenRepository tokenRepository) {
		super(key, userDetailsService);
		this.random = new SecureRandom();
		this.tokenRepository = tokenRepository;
	}

	/**
	 * 持久化方式验证登录
	 *
	 * @param cookieTokens 解密后的cookie数组
	 * @param request      请求对象
	 * @param response     响应对象
	 * @return 验证成功user对象
	 */
	@Override
	protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
			HttpServletResponse response) {
		// cookie长度校验
		if (cookieTokens.length != 2) {
			throw new InvalidCookieException("Cookie 长度不对，当前cookie为 '" + Arrays.asList(cookieTokens) + "'");
		}
		// 获取持久化可key 并尝试获取持久化值
		String presentedSeries = cookieTokens[0];
		String presentedToken = cookieTokens[1];
		// 获取不到判断
		PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
		if (token == null) {
			throw new RememberMeAuthenticationException(String.format("使用种子ID %s 未找到值: ", presentedSeries));
		}
		// 验签
		if (!presentedToken.equals(token.getTokenValue())) {
			// 验签失败 删除 持久化值 抛出异常
			this.tokenRepository.removeUserTokens(token.getUsername());
			throw new CookieTheftException(this.messages.getMessage(
					"PersistentTokenBasedRememberMeServices.cookieStolen", "无效的持久化token"));
		}
		// 超时验证
		if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
			throw new RememberMeAuthenticationException("记住我超时");
		}
		// 验签成功
		this.logger.debug(LogMessage.format("用户刷新持久化token '%s', series '%s'",
				token.getUsername(), token.getSeries()));
		// 生成新的token
		PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(),
				generateTokenData(), new Date());
		try {
			// 尝试存入token并新增cookie
			this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
			addCookie(newToken, request, response);
		} catch (Exception ex) {
			this.logger.error("更新token异常: ", ex);
			throw new RememberMeAuthenticationException("更新token异常");
		}
		// 从数据源中获取user对象
		return getUserDetailsService().loadUserByUsername(token.getUsername());
	}

	/**
	 * 登录成功回调 这里又重新持久化token
	 *
	 * @param request                  请求都西昂
	 * @param response                 响应对象
	 * @param successfulAuthentication 认证成功封装类
	 */
	@Override
	protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication successfulAuthentication) {
		String username = successfulAuthentication.getName();
		this.logger.debug(LogMessage.format("用户 %s创建持久化token", username));
		PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(),
				generateTokenData(), new Date());
		try {
			this.tokenRepository.createNewToken(persistentToken);
			addCookie(persistentToken, request, response);
		} catch (Exception ex) {
			this.logger.error("Failed to save persistent token ", ex);
		}
	}

	/**
	 * 立即登出，删除持久化token
	 *
	 * @param request        请求对象
	 * @param response       响应对象
	 * @param authentication 认证成功对象
	 */
	@Override
	public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
		super.logout(request, response, authentication);
		if (authentication != null) {
			this.tokenRepository.removeUserTokens(authentication.getName());
		}
	}

	/**
	 * 获取新的种子值
	 *
	 * @return 种子值 随机数
	 */
	protected String generateSeriesData() {
		byte[] newSeries = new byte[this.seriesLength];
		this.random.nextBytes(newSeries);
		return new String(Base64.getEncoder().encode(newSeries));
	}

	/**
	 * 生成新的token 随机数
	 *
	 * @return 随机数
	 */
	protected String generateTokenData() {
		byte[] newToken = new byte[this.tokenLength];
		this.random.nextBytes(newToken);
		return new String(Base64.getEncoder().encode(newToken));
	}

	/**
	 * 新增cookie
	 *
	 * @param token    token对象
	 * @param request  请求对象
	 * @param response 响应对象
	 */
	private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
		setCookie(new String[]{token.getSeries(), token.getTokenValue()}, getTokenValiditySeconds(), request,
				response);
	}

	/**
	 * 设置种子长度
	 *
	 * @param seriesLength 随机数长度
	 */
	public void setSeriesLength(int seriesLength) {
		this.seriesLength = seriesLength;
	}

	/**
	 * 设置token长度
	 *
	 * @param tokenLength token长度
	 */
	public void setTokenLength(int tokenLength) {
		this.tokenLength = tokenLength;
	}

	/**
	 * 设置token存活时间
	 *
	 * @param tokenValiditySeconds 设置值
	 */
	@Override
	public void setTokenValiditySeconds(int tokenValiditySeconds) {
		Assert.isTrue(tokenValiditySeconds > 0, "tokenValiditySeconds must be positive for this implementation");
		super.setTokenValiditySeconds(tokenValiditySeconds);
	}

}
